package dk.itu.mario.engine.JFugue;

import java.util.ArrayList;

import org.jfugue.Pattern;
import org.jfugue.elements.Note;

public class MusicParserForMarioLevelGen {

	Pattern pattern;
	String[] tokens;
	ArrayList<LayeredNote> temporalUnits = new ArrayList<LayeredNote>();
	long cumulativeTime = 0;
	
	public static final int WHOLE = 384;
	public static final int HALF = 192;
	public static final int QUARTER = 96;
	public static final int EIGHTH = 48;
	public static final int SIXTEENTH = 24;
	public static final int THIRTY_SECONDTH = 12;
	public static final int SIXTY_FOURTH = 6;
	public static final int ONE_TWENTY_EIGHTH = 3;
	
	public MusicParserForMarioLevelGen(Pattern pattern)
	{
		this.pattern = pattern;
		tokens = this.pattern.getTokens();
		parse();
	}
	
	public MusicParserForMarioLevelGen(ArrayList<LayeredNote> noteList)
	{
		temporalUnits.clear();
		if(!noteList.isEmpty())
		{
			temporalUnits.addAll(noteList);
		}
	}
	
	public void parse()
	{
//		System.out.println("Before Parsing:\n");
		
		for(int i = 0; i < tokens.length;)
		{
			i = parseNote(i);
		}
		
//		addRests();
		
//		System.out.println("After Parsing:\n");
		
//		for(int i = 1; i < temporalUnits.size(); i++)
//		{
//			System.out.println(i + " : " + temporalUnits.get(i).toString());
//			System.out.println(i - 1 + " - " + i + " : " + (temporalUnits.get(i).getTime() - temporalUnits.get(i - 1).getTime()) + ", " + temporalUnits.get(i).toString());
//		}
	}
	
	public int parseNote(int index)
	{
		long time;
		Note note;
		
		while(index < tokens.length && tokens[index].charAt(0) != '@')
		{
//			System.out.println(index + " : " + tokens[index]);
			
			index++;
		}
		
		if(index >= tokens.length)
		{
			return index;
		}
		
//		System.out.println(index + " : " + tokens[index]);
		
		//Handle Time Info
		time = Long.parseLong(tokens[index++].substring(1));
		
		time = quantize(time);
		
		while(index < tokens.length && (tokens[index].charAt(0) != '@' && tokens[index].charAt(0) != 'A' && tokens[index].charAt(0) != 'B'
				&& tokens[index].charAt(0) != 'C' && tokens[index].charAt(0) != 'D' && tokens[index].charAt(0) != 'E'
				&& tokens[index].charAt(0) != 'F' && tokens[index].charAt(0) != 'G'))
		{
//			System.out.println(index + " : " + tokens[index]);
			
			index++;
		}
		
		if(index == tokens.length || tokens[index].charAt(0) == '@')
		{
			return index;
		}
		
		//Handle Note Info
		note = parseNoteComponents(tokens[index]);
		
		//Insert Into Temporal Units ArrayList
		insertIntoTemporalUnits(time, note);
		
		return ++index;
	}
	
	public Note parseNoteComponents(String noteString)
	{
		int index = 0;
		double decimalDuration = 0;
		byte octaveByte;
		byte value = 0, attackVelocity = 60, decayVelocity = 60;
		String noteValue = "", octave = "", duration = "", attackV = "60", decayV = "60";
		
		//Parse Note String
		while(index < noteString.length() && (noteString.charAt(index) == 'A' || noteString.charAt(index) == 'B' || noteString.charAt(index) == 'C'
				|| noteString.charAt(index) == 'D' || noteString.charAt(index) == 'E'
				|| noteString.charAt(index) == 'F' || noteString.charAt(index) == 'G'
				|| noteString.charAt(index) == '#' || noteString.charAt(index) == 'b'))
		{
			noteValue += noteString.charAt(index++);
		}
		
		octave += noteString.charAt(index++);
		
		if(index < noteString.length() && Character.isLetter(noteString.charAt(index)))
		{
			duration += noteString.charAt(index++);
			
			decimalDuration = Note.getDecimalForDuration(duration);
		}
		
		else
		{
			if(index < noteString.length() && noteString.charAt(index) == '/')
			{
				index++;
			}
			
			while(index < noteString.length() && (Character.isDigit(noteString.charAt(index)) || noteString.charAt(index) == '.'))
			{
				duration += noteString.charAt(index++);
			}
			
			decimalDuration = Double.parseDouble(duration);
		}
		
		if(index < noteString.length() && noteString.charAt(index) == 'a')
		{
			attackV = "";
			index++;
			while(index < noteString.length() && Character.isDigit(noteString.charAt(index)))
			{
				attackV += noteString.charAt(index++);
			}
		}
		
		if(index < noteString.length() && noteString.charAt(index) == 'd')
		{
			decayV = "";
			index++;
			while(index < noteString.length() && Character.isDigit(noteString.charAt(index)))
			{
				decayV += noteString.charAt(index++);
			}
		}
		
//		System.out.println("Duration : " + duration + ", Note Value : " + noteValue + ", Octave : "
//				+ octave + ", Attack Velocity : " + attackV + ", Decay Velocity : " + decayV);
		
		attackVelocity = Byte.parseByte(attackV);
		decayVelocity = Byte.parseByte(decayV);
		octaveByte = Byte.parseByte(octave);
		value = getByteNoteValueFromString(noteValue, octaveByte);
		
		return new Note(value, decimalDuration, attackVelocity, decayVelocity);
	}
	
	public static byte getByteNoteValueFromString(String noteAndOctave)
	{
		int index = 0;
		String noteValue = "", octave = "";
		byte octaveByte = 0;
		//Parse Note String
		while(index < noteAndOctave.length() && (noteAndOctave.charAt(index) == 'A'
				|| noteAndOctave.charAt(index) == 'B' || noteAndOctave.charAt(index) == 'C'
				|| noteAndOctave.charAt(index) == 'D' || noteAndOctave.charAt(index) == 'E'
				|| noteAndOctave.charAt(index) == 'F' || noteAndOctave.charAt(index) == 'G'
				|| noteAndOctave.charAt(index) == '#' || noteAndOctave.charAt(index) == 'b'))
		{
			noteValue += noteAndOctave.charAt(index++);
		}
		
		while(index < noteAndOctave.length() && Character.isDigit(noteAndOctave.charAt(index)))
		{
			octave += noteAndOctave.charAt(index++);
		}
		octaveByte = Byte.parseByte(octave);
		
		
		return getByteNoteValueFromString(noteAndOctave, octaveByte);
	}
	
	public static byte getByteNoteValueFromString(String noteString, byte octave)
	{
		byte value = -1;
		
		for(int i = 0; i < Note.NOTES.length; i++)
		{
			if(noteString.equalsIgnoreCase(Note.NOTES[i]))
			{
				value = (byte) (octave * 12 + i);
				break;
			}
		}
		
		if(value < 0)
		{
			System.out.println("Error: Note String was corrupt");
		}
		
		return value;
	}
	
	public void insertIntoTemporalUnits(long time, Note note)
	{
		if(temporalUnits.size() == 0)
		{
			ArrayList<Note> noteList = new ArrayList<Note>();
			noteList.add(note);
			temporalUnits.add(new LayeredNote(time, noteList));
			return;
		}
		
		for(int i = 0; i < temporalUnits.size(); i++)
		{
			if(time == temporalUnits.get(i).getTime())
			{
				temporalUnits.get(i).appendNote(note);
				return;
			}
			else if(time < temporalUnits.get(i).getTime())
			{
				ArrayList<Note> noteList = new ArrayList<Note>();
				noteList.add(note);
				temporalUnits.add(i, new LayeredNote(time, noteList));
				return;
			}
			else if(i == temporalUnits.size() - 1)
			{
				ArrayList<Note> noteList = new ArrayList<Note>();
				noteList.add(note);
				temporalUnits.add(new LayeredNote(time, noteList));
				return;
			}
		}
	}
	
	//Rounds to nearest multiple of 12 milliseconds
	public long quantize(long time)
	{
		long remainder = time % 12;
		
		if(remainder <= 6)
		{
			time -= remainder;
		}
		else
		{
			time += (12 - remainder);
		}
		
		return time;
	}
	
	public void addRests()
	{
		for(int i = 1; i < temporalUnits.size(); i++)
		{
			if(temporalUnits.get(i).getTime() - temporalUnits.get(i - 1).getTime() > 96)
			{
				long time = temporalUnits.get(i - 1).getTime() + 96;
				Note note = new Note();
				note.setRest(true);
				note.setDecimalDuration(0.25);
				
				ArrayList<Note> noteList = new ArrayList<Note>();
				noteList.add(note);
				temporalUnits.add(i, new LayeredNote(time, noteList));
			}
		}
	}
	
	public long getTotalTime()
	{
		if(!temporalUnits.isEmpty())
		{
			return temporalUnits.get(temporalUnits.size() - 1).getTime();
		}
		
		return 0;
	}
	
	public ArrayList<LayeredNote> getNoteListForNoteAndOctave(String noteAndOctave)
	{
		int noteValue = getByteNoteValueFromString(noteAndOctave);
		
		return getNoteListFiltered(noteValue);
	}
	
	public ArrayList<LayeredNote> getNoteListForDrumInstrument(String drumInstrument)
	{
		int noteValue = LayeredNote.PercValueFromName(drumInstrument);
		
		return getNoteListFiltered(noteValue);
	}
	
	public ArrayList<LayeredNote> getNoteListFiltered(int noteValue)
	{
		ArrayList<LayeredNote> newNoteList = new ArrayList<LayeredNote>();
		
		for(LayeredNote layer : temporalUnits)
		{
			newNoteList.add(layer.findSimultaneousNotes(noteValue));
		}
		
		return newNoteList;
	}

	/**
	 * @return the temporalUnits
	 */
	public ArrayList<LayeredNote> getTemporalUnits() {
		return temporalUnits;
	}

	/**
	 * @param temporalUnits the temporalUnits to set
	 */
	public void setTemporalUnits(ArrayList<LayeredNote> temporalUnits) {
		this.temporalUnits = temporalUnits;
	}
	
	public String getMusicString(int startIndex, int endIndex)
	{
		String result = "";
		
		for(int index = startIndex; index <= endIndex; index++)
		{
			result += "@" + temporalUnits.get(index).getTime() + " " + temporalUnits.get(index).toMusicString();
		}
		
//		System.out.println(result);
		return result;
	}
	
	public String toString()
	{
		String result = "";
		
		for(LayeredNote layer : temporalUnits)
		{
			result += layer.toString() + "\n";
		}
		
		return result;
	}
}
